#include "Pch.h"

#include "../../MCD/Core/System/Path.h"
#include "../../MCD/Core/System/WindowEvent.h"
#include "../../MCD/Core/System/Thread.h"

#include "../../MCD/Render/Effect.h"
#include "../../MCD/Render/Material.h"
#include "../../MCD/Render/ShaderProgram.h"
#include "../../MCD/Render/RenderBufferSet.h"
#include "../../MCD/Render/Model.h"
#include "../../MCD/Render/EditableMesh.h"
#include "../../MCD/Render/RayMeshIntersect.h"

#include "../Fbx/FbxFile.h"
#include "../Fbx/ModelImporter.h"

#include "../Core/RayTrace/SphericalSamplers.h"
#include "../Core/Math/Common.h"
#include "../Core/Application.h"
#include "../Core/DefaultResourceManager.h"

#include "GlVisualizer.h"

#include <vector>

using namespace IS;

namespace TestPhotonSplatting
{

class CameraSampler
{
public:
	Camera& mCamera;

	CameraSampler(Camera& camera)
		: mCamera(camera)
	{
	}

	void generateN(size_t n, std::vector<Vec3f>& outs)
	{
		if(0 == n) return;

		outs.clear();

		Vec3f lookDir = mCamera.lookAtDir();
		float cosFov = cosf(mCamera.frustum.fov());

		do
		{
			double x = double(rand()) / double(RAND_MAX);
			double y = double(rand()) / double(RAND_MAX);

			x = x * 2 - 1;
			y = y * 2 - 1;

			if(x*x + y*y > 1) continue;

			double z = sqrt(1.0 - x*x - y*y);

			double s = double(rand()) / double(RAND_MAX);
			if(s < 0.5) z = -z;

			Vec3f v((float)x, (float)y, (float)z);

			if(v.dot(lookDir) < cosFov) continue;

			outs.push_back(v);
		}
		while(outs.size() < n);
	}
};

class Helper
{
public:
	static EffectPtr loadEffect(DefaultResourceManager& resMgr, const wchar_t* filepath)
	{
		EffectPtr eff = (Effect*)resMgr.load(filepath, true).get();

		if(nullptr != eff)
		{
			while(!eff->material.get())
				resMgr.processLoadingEvents();
		}

		return eff;
	}

	static void bindRB(RenderBufferSet& rbufs, size_t bufIdx, Effect& effect, size_t passIdx, size_t texIdx)
	{
		if(passIdx >= effect.material->getPassCount()) return;

		TextureProperty* prop = effect.material->mRenderPasses[passIdx].textureProp(texIdx);
		if(nullptr == prop) return;

		prop->texture = rbufs.bufferInfo(bufIdx).texture();
	}
};


class TestApp : public Application
{
public:
	ModelPtr mModel;
	Vec3f mLightPos;
	Vec3f mLightClr;
	bool mDirectIllum, mIndirectIllum;
	EffectPtr mGBuffEffect, mDirLtEffect, mIllumApplyEffect, mSplatEffect, mIllumAccumEffect;

	std::auto_ptr<RenderBufferSet> mRendBufs;

	enum RenderBufferId
	{
		E_NormalBuffer,
		E_NormalBuffer2,
		E_PositionBuffer,
		E_IllumBuffer,
		E_IllumBuffer2,
		E_TempBuffer,
	};

	RenderBufferId mIllumBuffIndex[2], mNormalBuffIndex[2];

	RandomSphericalSamplerUniform mSamplerUniform;
	CameraSampler mSamplerCamera;

	struct Photon
	{
		Vec3f hitPos;
		Vec3f hitNormal;
		Vec4f hitDiffuse;
		Vec4f wattage;
		bool active;
	};

	struct PhotonHistory : public LinkListBase::Node<PhotonHistory>
	{
		Vec3f hitPos;
		Vec3f hitNormal;
		Vec4f hitDiffuse;
	};

	Mat44f mLastViewProjMat;

public:
	TestApp()
		: Application(L"title=TestPhotonSplatting;width=800;height=600;FSAA=1")
		, mLightPos(0, 15, 0)
		, mLightClr(1, 1, 1)
		, mDirectIllum(true)
		, mIndirectIllum(true)
		, mSamplerCamera(mCamera)
	{
		mIllumBuffIndex[0] = E_IllumBuffer;
		mIllumBuffIndex[1] = E_IllumBuffer2;

		mNormalBuffIndex[0] = E_NormalBuffer;
		mNormalBuffIndex[1] = E_NormalBuffer2;

		mCamera.moveForward(0.0f);
		mCamera.moveUp(5.0f);

		// load effects
		mGBuffEffect = Helper::loadEffect(mResMgr, L"gbuffersetup.fx.xml");
		mDirLtEffect = Helper::loadEffect(mResMgr, L"directlighting.fx.xml");
		mSplatEffect = Helper::loadEffect(mResMgr, L"photonsplatting.fx.xml");
		mIllumApplyEffect = Helper::loadEffect(mResMgr, L"applyillum.fx.xml");
		mIllumAccumEffect = Helper::loadEffect(mResMgr, L"accumillum.fx.xml");

		// open fbx file
		std::wstring fbxfilepath = (Path::getCurrentPath() / L"media/BoxRoom.FBX").getString();
		//std::wstring fbxfilepath = (Path::getCurrentPath() / L"media/scene02_cave_master.fbx").getString();

		Log::format( Log::Info, L"Importing %s ...", fbxfilepath.c_str() );

		FbxFile fbxfile(fbxfilepath.c_str());

		if(!fbxfile.open())
			return;

		Log::format( Log::Info, L"FBX file version %d.%d.%d"
			, fbxfile.fileVersion().major
			, fbxfile.fileVersion().minor
			, fbxfile.fileVersion().revision
			);

		// import the model from fbx
		mModel = new Model(fbxfilepath);
		ModelImporter().import(fbxfile, *mModel);

		initRenderBuffers(mResMgr);

		clearIllumBuffers();
	}

	void initRenderBuffers(IResourceManager& resMgr)
	{
		mRendBufs.reset(new RenderBufferSet(resMgr, this->width(), this->height(), RenderBufferSet::DepthBuffer_Texture32, false));
		mRendBufs->textureBuffer(GL_RGBA32F_ARB, L"normal.rtt");
		mRendBufs->textureBuffer(GL_RGBA32F_ARB, L"normal2.rtt");
		mRendBufs->textureBuffer(GL_RGBA32F_ARB, L"position.rtt");
		mRendBufs->textureBuffer(GL_RGBA32F_ARB, L"ilumin.rtt");
		mRendBufs->textureBuffer(GL_RGBA32F_ARB, L"ilumin2.rtt");
		mRendBufs->textureBuffer(GL_RGBA32F_ARB, L"temp.rtt");
		mRendBufs->checkDeviceStatus(true);
	}
	
	void clearIllumBuffers()
	{
		for(int i=0; i<2; ++i)
		{
			mRendBufs->begin(mIllumBuffIndex[i]);
			mRendBufs->checkDeviceStatus(false);

			glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
			glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

			mRendBufs->end();
		}
	}

	void drawScene(Effect& effect)
	{
		/*
		for(TriangleBuffer* tbuf = mTriBufs.begin(); tbuf != mTriBufs.end(); tbuf = tbuf->next())
		{
			glPushMatrix();
			glMultTransposeMatrixf(tbuf->localTransform.getPtr());

			if(&effect == mGBuffEffect.get())
			{
				effect.material->preRender(0);
				ShaderProgram::current()->uniformMatrix4fv("g_WorldMatrix", 1, true, tbuf->localTransform.getPtr());
				ShaderProgram::current()->uniform1f("g_TextureSpace", 0.0f);
				tbuf->draw();
				effect.material->postRender(0);
			}
			else
			{
				effect.material->preRender(0);
				tbuf->draw();
				effect.material->postRender(0);
			}

			glPopMatrix();
		}
		*/

		
		for(Model::MeshAndMaterial* meshAndMat = mModel->mMeshes.begin(); meshAndMat != mModel->mMeshes.end(); meshAndMat = meshAndMat->next())
		{
			if(!meshAndMat->mesh || !meshAndMat->material.get())
				continue;

			size_t passCount = meshAndMat->material->getPassCount();
			for(size_t passId = 0; passId < passCount; ++passId)
			{
				meshAndMat->material->preRender(passId);

				effect.material->preRender(0);

				if(&effect == mGBuffEffect.get())
				{
					ShaderProgram::current()->uniformMatrix4fv("g_WorldMatrix", 1, true, Mat44f::cIdentity.data);
					ShaderProgram::current()->uniform1f("g_TextureSpace", 0.0f);
				}

				meshAndMat->mesh->draw();

				effect.material->postRender(0);

				meshAndMat->material->postRender(passId);
			}
		}
	}

	void drawSceneToScreen()
	{
		Helper::bindRB(*mRendBufs, mIllumBuffIndex[0], *mIllumApplyEffect, 0, 0);
		//Helper::bindRB(*mRendBufs, E_TempBuffer, *mIllumApplyEffect, 0, 0);

		glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

		mIllumApplyEffect->material->preRender(0);
		drawScene(*mIllumApplyEffect);
		mIllumApplyEffect->material->postRender(0);
	}

	void drawSceneToGBuffer()
	{
		mRendBufs->begin(mNormalBuffIndex[0], E_PositionBuffer);
		mRendBufs->checkDeviceStatus(false);

		glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

		drawScene(*mGBuffEffect);

		mRendBufs->end();
	}


	void shootPhotons(Photon* photons, const size_t photonCnt, bool bounce)
	{
		static std::vector<Vec3f> samples;
		
		if(samples.capacity() < photonCnt)
			samples.reserve(photonCnt);

		samples.clear();
		//mSamplerUniform.generateN(photonCnt, samples);
		mSamplerCamera.generateN(photonCnt, samples);

		SimpleRayMeshIntersect tbi;

		for(Model::MeshAndMaterial* meshAndMat = mModel->mMeshes.begin(); meshAndMat != mModel->mMeshes.end(); meshAndMat = meshAndMat->next())
		{
			if(!meshAndMat->mesh || !meshAndMat->material.get())
				continue;

			EditableMesh* mesh = (EditableMesh*)meshAndMat->mesh.get();
			mesh->beginEditing();
			tbi.addMesh(mesh);
		}

		tbi.build();
		tbi.begin();

		const float cOffset = 0.1f;
		const float cRandNormScale = 0.25f;
		const float cDiffuseReflectivity = 1.0f;

		for(size_t i=0; i<photonCnt; ++i)
		{
			Photon& p = photons[i];

			Vec3f rayOrig = mCamera.position;//mLightPos;
			Vec3f rayDir = samples[i];

			if(bounce)
			{
				if(samples[i].dot(p.hitNormal) < 0)
					samples[i] = -samples[i];
				samples[i] = (cRandNormScale * p.hitNormal + samples[i]).normalizedCopy();
				
				rayOrig = p.hitPos;
				rayDir = samples[i];
			}
			
			tbi.test(rayOrig, rayDir, false);
		}
		
		tbi.end();

		IRayMeshIntersect::HitResult* results = tbi.results().begin();

		for(size_t i=0; i<photonCnt; ++i)
		{
			Photon& p = photons[i];

			if(results->hits.isEmpty())
			{
				p.active = false;
				continue;
			}

			p.active = true;
			p.hitNormal = IRayMeshIntersect::Helper::getHitNormal(results->closest);
			p.hitPos = IRayMeshIntersect::Helper::getHitPosition(results->closest) + p.hitNormal * cOffset;

			if(bounce)
			{
				p.wattage.x *= p.hitDiffuse.x * cDiffuseReflectivity;
				p.wattage.y *= p.hitDiffuse.y * cDiffuseReflectivity;
				p.wattage.z *= p.hitDiffuse.z * cDiffuseReflectivity;
			}
			else
			{
				p.wattage = Vec4f(mLightClr.x, mLightClr.y, mLightClr.z, 1);
			}

			p.hitDiffuse = IRayMeshIntersect::Helper::getHitUV4d(results->closest, 3);
			//p.hitDiffuse.x *= p.hitDiffuse.x;
			//p.hitDiffuse.y *= p.hitDiffuse.y;
			//p.hitDiffuse.z *= p.hitDiffuse.z;

			// next
			results = results->next();
		}

		for(Model::MeshAndMaterial* meshAndMat = mModel->mMeshes.begin(); meshAndMat != mModel->mMeshes.end(); meshAndMat = meshAndMat->next())
		{
			if(!meshAndMat->mesh || !meshAndMat->material.get())
				continue;

			EditableMesh* mesh = (EditableMesh*)meshAndMat->mesh.get();
			mesh->endEditing(false);
		}
	}

	void splatPhotons(const Photon* photons, const size_t photonCnt)
	{
		Helper::bindRB(*mRendBufs, mNormalBuffIndex[0], *mSplatEffect, 0, 0);
		Helper::bindRB(*mRendBufs, E_PositionBuffer, *mSplatEffect, 0, 1);

		mSplatEffect->material->preRender(0);

		static std::vector<Vec4f> mPhotonPos;
		static std::vector<Vec4f> mPhotonDir;
		static std::vector<Vec4f> mPhotonClr;

		if(mPhotonPos.size() < photonCnt) mPhotonPos.resize(photonCnt);
		if(mPhotonDir.size() < photonCnt) mPhotonDir.resize(photonCnt);
		if(mPhotonClr.size() < photonCnt) mPhotonClr.resize(photonCnt);

		for(size_t i=0; i<photonCnt; ++i)
		{
			// some init value
			mPhotonClr[i] = Vec4f(0, 0, 0, 0);

			const Photon& p = photons[i];
			if(!p.active) continue;

			mPhotonPos[i] = Vec4f(p.hitPos, 0);
			mPhotonDir[i] = Vec4f(p.hitNormal, 0);
			mPhotonClr[i] = p.wattage;
		}

		ShaderProgram::current()->uniform4fv("g_photonPosition", photonCnt, (float*)(mPhotonPos[0].data));
		ShaderProgram::current()->uniform4fv("g_photonDirection", photonCnt, (float*)mPhotonDir[0].data);
		ShaderProgram::current()->uniform4fv("g_photonColor", photonCnt, (float*)(mPhotonClr[0].data));
		
		ScreenQuad::draw(mRendBufs->target(), 0, 0, mRendBufs->width(), mRendBufs->height());

		mSplatEffect->material->postRender(0);
	}

	/**/
	void accumulatePhotons(size_t photonCnt)
	{
		Helper::bindRB(*mRendBufs, mNormalBuffIndex[0], *mIllumAccumEffect, 0, 0);
		Helper::bindRB(*mRendBufs, E_PositionBuffer, *mIllumAccumEffect, 0, 1);
		Helper::bindRB(*mRendBufs, E_TempBuffer, *mIllumAccumEffect, 0, 2);
		Helper::bindRB(*mRendBufs, mIllumBuffIndex[1], *mIllumAccumEffect, 0, 3);
		Helper::bindRB(*mRendBufs, mNormalBuffIndex[1], *mIllumAccumEffect, 0, 4);

		mRendBufs->begin(mIllumBuffIndex[0]);
		mRendBufs->checkDeviceStatus(false);

		mIllumAccumEffect->material->preRender(0);

		ShaderProgram::current()->uniformMatrix4fv("g_CacheMatrix", 1, true, mLastViewProjMat.getPtr());
		ShaderProgram::current()->uniform1f("g_photonEmitCnt", (float)photonCnt);
		
		ScreenQuad::draw(mRendBufs->target(), 0, 0, mRendBufs->width(), mRendBufs->height());

		mIllumAccumEffect->material->postRender(0);
		
		mRendBufs->end();
	}

	void drawPhotons()
	{
		glPushAttrib(GL_LIGHTING_BIT|GL_DEPTH_BUFFER_BIT);
		//glDisable(GL_DEPTH_TEST);
		glDisable(GL_LIGHTING);

		glBegin(GL_LINES);

		const float s = 0.1f;

		glColor3f(0.0f, 1.0f, 0.0f);
		GlVisualizer::drawPoint(mLightPos, s * 5.0f);

		glEnd();

		glPopAttrib();
	}
	

	sal_override void update(float deltaTime)
	{
		// prepare geometry buffer
		drawSceneToGBuffer();

		// shoot photons
		const size_t cPhotonCnt = 32;
		{
			mRendBufs->begin(E_TempBuffer);
			mRendBufs->checkDeviceStatus(false);

			glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
			glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

			Photon photons[cPhotonCnt];

			shootPhotons(photons, cPhotonCnt, false);
			if(mDirectIllum)
				splatPhotons(photons, cPhotonCnt);

			// bouncing
			if(mIndirectIllum)
			{
				for(int i=0; i<2; ++i)
				{
					shootPhotons(photons, cPhotonCnt, true);
					splatPhotons(photons, cPhotonCnt);
				}
			}
			
			mRendBufs->end();
		}
		accumulatePhotons(cPhotonCnt);

		drawSceneToScreen();

		drawPhotons();
		
		// swap illum buffers
		std::swap(mIllumBuffIndex[0], mIllumBuffIndex[1]);
		std::swap(mNormalBuffIndex[0], mNormalBuffIndex[1]);

		Mat44f viewMat, projMat;
		mCamera.computeView(viewMat.data);
		mCamera.frustum.computeProjection(projMat.data);

		mLastViewProjMat = projMat * viewMat;
	}

	sal_override void onEvent(const MCD::Event& e)
	{
		Application::onEvent(e);

		if(Event::KeyPressed == e.Type && Key::H == e.Key.Code)
		{
			wprintf(L"F1-restart\nF2-display photons\nF3-apply direct illumination photons\n");
		}
		
		if(Event::KeyPressed == e.Type && Key::F1 == e.Key.Code)
			clearIllumBuffers();

		if(Event::KeyPressed == e.Type && Key::F2 == e.Key.Code)
		{
			mIndirectIllum = !mIndirectIllum;
			wprintf(L"mIndirectIllum = %s\n", (mIndirectIllum?L"true":L"false") );
		}

		if(Event::KeyPressed == e.Type && Key::F3 == e.Key.Code)
		{
			mDirectIllum = !mDirectIllum;
			wprintf(L"mDirectIllum = %s\n", (mDirectIllum?L"true":L"false") );
		}
		/*
		if(Event::KeyPressed == e.Type && Key::F2 == e.Key.Code)
		{
			mDrawPhotons = !mDrawPhotons;
			//wprintf(L"mDrawPhotons = %s\n", (mDrawPhotons?L"true":L"false") );
		}
		*/
		
		
	}

};	// class TestApp

}	// namespace TestPhotonSplatting

TEST(TestPhotonSplatting)
{
	TestPhotonSplatting::TestApp app;
	app.mainLoop();
}